Java 面试题积累
数值取值范围相关
因为这类问题太普遍了,所以这里记录一下
顺便补充一下取模操作,免得老是忘记...
5 % 2 = 1,
1 % 2 = 1
String 是否有拆箱?
String str = new String("hello");
System.out.println(str == "hello");
返回的是 false,因为 String 类型不是基本类型,所以不存在拆箱这一操作
finally 是否会在 return 后执行
下面程序的输出结果为( )
public class Demo {
public static String sRet = "";
public static void func(int i) {
try {
// 5 % 2 = 1,
// 1 % 2 = 1
if (i % 2 == 0) {
throw new Exception();
}
} catch (Exception e) {
sRet += "0";
return;
} finally {
sRet += "1";
}
sRet += "2";
}
public static void main(String[] args) {
func(1);
func(2);
System.out.println(sRet);
}
}
A、120 B、1201 C、12012 D、101
- 调用 func(1) ,if 不符合,直接进入 finally,sRet="1"
- finally 语句中没有返回值,故继续向下执行,sRet="12"
- 调用 func(2) ,if符合,sRet = "120",此时有返回值!!!
- 调用 finally 语句,sRet="1201"
- 因为已经有返回值了,finally 之后的语句也不再执行,sRet="1201"。
这题的关键就是 finally 是否会在 return 后执行?
答案是肯定的,在 try 执行完成之后,finally 是一定会执行的。这种特性可以让程序员避免在 try 语句中使用了 return,continue 或者 break 关键字而忽略了关闭相关资源的操作。
PS: 用到 finally 关闭资源的时候,应该尽量避免在 finally 语句块中出现运行时错误,可以适当添加判断语句以增加程序健壮性:
finally {
if (out != null) {
System.out.println("Closing PrintWriter");
out.close(); // 不要在finally语句中直接调用close()
} else {
System.out.println("PrintWriter not open");
}
}
finalize 方法的执行
finalize 是 Object 类的一个方法,在垃圾回收器执行时会调用被回收对象的 finalize()
方法,可以覆盖此方法来实现对其他资源的回收(一旦垃圾回收器准备好释放对象占用的空间,将首先调用该方法,并且在下一次垃圾回收动作发生时,才会真正回收对象占用的内存),从功能上来说,finalize()
方法与 c++ 中的析构函数比较相似,但是 Java 采用的是基于垃圾回收器的自动内存管理机制,所以 finalize()
方法在本质上不同于 C++ 中的析构函数。
判定一个对象 objA 是否可回收,至少要经历两次标记过程:
1、如果对象 objA 到 GC Roots 没有引用链,则进行第一次标记。
进行筛选,判断此对象是否有必要执行 finalize()
方法
如果对象 objA 没有重写 finalize()
方法,或者 finalize()
方法已经被虚拟机调用过,则虚拟机视为 “没有必要执行”,objA 被判定为不可触及的。
如果对象 objA 重写了 finalize()
方法,且还未执行过,那么 objA 会被插入到 F-Queue 队列中,由一个虚拟机自动创建的、低优先级的 Finalizer 线程触发其 finalize()
方法执行。
finalize()
方法是对象逃脱死亡的最后机会,稍后 GC 会对 F-Queue 队列中的对象进行第二次标记。如果 objA 在 finalize()
方法中与引用链上的任何一个对象建立了联系,那么在第二次标记时,objA 会被移出 “即将回收” 集合。
之后,对象会再次出现没有引用存在的情况。在这个情况下,finalize 方法不会被再次调用,对象会直接变成不可触及的状态,也就是说,一个对象的 finalize 方法只会被调用一次。
构造方法在被哪些地方调用?
问题:用户不能调用构造方法,只能通过 new
关键字自动调用。(错误)
解析:
1、在类内部可以用户可以使用关键字 this.构造方法名()
调用(参数决定调用的是本类对应的构造方法)
2、在子类中用户可以通过关键字 super.父类构造方法名()
调用(参数决定调用的是父类对应的构造方法。)
3、反射机制对于任意一个类,都能够知道这个类的所有属性和方法,包括类的构造方法。
字符串的垃圾回收
static String str0 = "0123456789";
static String str1 = "0123456789";
String str2 = str1.substring(5);
String str3 = new String(str2);
String str4 = new String(str3.toCharArray());
str0 = null;
假定 str0,...,str4 后序代码都是只读引用。在 Java 7 中,以上述代码为基础,在发生过一次 FullGC 后,上述代码在 Heap 空间(不包括 PermGen)保留的字符数为()
A、5 B、10 C、15 D、20
解析:这是一个关于 Java 的垃圾回收机制的题目。垃圾回收主要针对的是堆区的回收,因为栈区的内存是随着线程而释放的。堆区分为三个区:
- 年轻代(Young Generation)
- 年老代(Old Generation)
- 永久代(Permanent Generation,也就是方法区)。
年轻代:对象被创建时(new)的对象通常被放在 Young(除了一些占据内存比较大的对象),经过一定的 Minor GC(针对年轻代的内存回收)还活着的对象会被移动到年老代(一些具体的移动细节省略)。
年老代:就是上述年轻代移动过来的和一些比较大的对象。Major GC/Full GC 是针对年老代的回收
永久代:存储的是 final 常量,static 变量,常量池。
str3,str4 都是直接 new
的对象,而 substring 的源代码其实也是 new String
对象返回,而经过 fullgc 之后,年老区的内存回收,则年轻区的占了15个,不 PermGen。所以答案选 C
注意:str1 在常量池里,常量池是在 PermGen 中,不属于 Heap 空间,而这里是因为 str1.substring(5)
截取的字符串要重新放一个字符串到堆中
Java 构造代码块
public class HelloA {
public HelloA() {
System.out.println("A的构造函数");
}
{
System.out.println("A的构造代码块");
}
static {
System.out.println("A的静态代码块");
}
public static void main(String[] args) {
HelloA a = new HelloA();
}
}
关于构造代码块,以下几点要注意:
构造代码块的作用是给对象进行初始化。
1、对象一建立就运行构造代码块了,而且优先于构造函数执行。这里要强调一下,有对象建立,才会运行构造代码块,类不能调用构造代码块的,而且构造代码块与构造函数的执行顺序是前者先于后者执行。
2、构造代码块与构造函数的区别是:构造代码块是给所有对象进行统一初始化,而构造函数是给对应的对象初始化,因为构造函数是可以多个的,运行哪个构造函数就会建立什么样的对象,但无论建立哪个对象,都会先执行相同的构造代码块。也就是说,构造代码块中定义的是不同对象共性的初始化内容。
垃圾收集机制:新生代、老年代、持久代
现代垃圾收集器大部分都基于分代收集理论设计,堆空间细分为:
Java 7及之前堆内存逻辑上分为三部分:新生区 + 养老区 + 永久区
- Young Generation Space 新生区 Young/New 又被划分为 Eden区和 Survivor区
- Tenure Generation space 养老区 Old/Tenure
- Permanent Space 永久区 Perm
Java 8及之后堆内存逻辑上分为三部分:新生区 + 养老区 + 元空间
- Young Generation Space新生区 Young/New 又被划分为 Eden区和 Survivor区
- Tenure Generation space 养老区 Old/Tenure
- Meta Space 元空间 Meta
约定:下面几个名词代表的意思是一样的
- 新生区 = 新生代 = 年轻代
- 养老区 = 老年区 = 老年代
- 永久区 = 永久代
新生代:
- 所有对象创建在新生代的 Eden 区,当 Eden 区满后触发新生代的 Minor GC,将 Eden 区和非空闲 Survivor 区存活的对象复制到另外一个空闲的 Survivor 区中。
- 保证一个 Survivor 区是空的,新生代 Minor GC 就是在两个 Survivor 区之间相互复制存活对象,直到 Survivor 区满为止。
老年代:当 Survivor 区也满了之后就通过 Minor GC 将对象复制到老年代。老年代也满了的话,就将触发 Full GC,针对整个堆(包括新生代、老年代、持久代)进行垃圾回收。
持久代:持久代如果满了,将触发 Full GC。
基类构造方法的执行
下面代码的输出是什么?
public class Base {
private String baseName = "base";
public Base() {
callName();
}
public void callName() {
System. out. println(baseName);
}
static class Sub extends Base {
private String baseName = "sub";
public void callName() {
System. out. println (baseName) ;
}
}
public static void main(String[] args) {
Base b = new Sub();
}
}
这里的 new Sub();
在创造派生类的过程中首先创建基类对象,然后才能创建派生类。
创建基类即默认调用 Base()
方法,在方法中调用 callName()
方法,由于派生类中存在此方法,则被调用的 callName()
方法是派生类中的方法,此时派生类还未构造,所以变量 baseName 的值为 null
因此这里打印的是 null
substring 的取值范围
下面这个 b 的字符串是什么?
String a = "Hello";
String b = a.substring(0 , 2);
substring 方法后面跟的两个 int 值的索引下标是一个左闭右开的集合,即返回一个包含从 start 到最后(不包含 end)的子字符串的字符串。
所以这里 b 的字符串是 "He"
forward 和 redirect
下面有关 forward(转发)和 redirect(重定向)的描述,正确的是 () ?
A、forward 是服务器将控制权转交给另外一个内部服务器对象,由新的对象来全权负责响应用户的请求 B、执行 forward 时,浏览器不知道服务器发送的内容是从何处来,浏览器地址栏中还是原来的地址 C、执行 redirect 时,服务器端告诉浏览器重新去请求地址 D、forward 是内部重定向,redirect 是外部重定向 E、redirect 默认将产生 301 Permanently moved 的 HTTP 响应
答案是 B C D
从地址栏显示来说:
forward 是服务器请求资源,服务器直接访问目标地址的 URL,把那个 URL 的响应内容读取过来,然后把这些内容再发给浏览器。浏览器根本不知道服务器发送的内容从哪里来的,所以它的地址栏还是原来的地址。
redirect 是服务端根据逻辑,发送一个状态码,告诉浏览器重新去请求那个地址。所以地址栏显示的是新的 URL。
从数据共享来说:
- forward:转发页面和转发到的页面可以共享 request 里面的数据。
- redirect:不能共享数据。
从使用场景来说:
- forward:一般用于用户登陆的时候,根据角色转发到相应的模块。
- redirect:一般用于用户注销登陆时返回主页面和跳转到其它的网站等。
从效率来说:
- forward:高
- redirect:低
Class 文件规范相关问题
- JAVA 程序的 main 方法必须写在类里面
- JAVA 程序中 public 修饰的类名必须与文件名一样
- JAVA 程序的 main 方法中,不管有多少条语句都必须用
{}
(大括号)括起来 - Java 中基本的编程单元为类(不是函数)